// {{ .Data.GenerateTitle }}
package main

import (
	"errors"
	"fmt"
	_ "{{ .PkgName }}/api"
	"gitee.com/kristas/booting-go/framework/common/util/lang"
	"gitee.com/kristas/booting-go/framework/common/util/list"
	"gitee.com/kristas/booting-go/framework/common/util/reflectx"
	"gitee.com/kristas/booting-go/framework/core/bean"
	"gitee.com/kristas/booting-go/framework/core/ioc/bean_factory"
	"gitee.com/kristas/booting-go/framework/core/ioc/bean_repository"
	"gitee.com/kristas/booting-go/framework/core/statement/types"
	"gitee.com/kristas/booting-go/framework/web"
	"gitee.com/kristas/booting-go/framework/web/controller"
	"gitee.com/kristas/booting-go/gen_tool/booctl/util"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"strings"
)

type ApiGenerator struct {
	types.Component
	lock *WdLock
	root string
	cmd  util.GoCmd
}

func (a *ApiGenerator) Order() int {
	return 1
}

func (a *ApiGenerator) Async() bool {
	return false
}

func NewApiGenerator(lock *WdLock) *ApiGenerator {
	return &ApiGenerator{lock: lock}
}

func (a *ApiGenerator) Init() error {
	return nil
}

func (a *ApiGenerator) Filter(bean bean.Bean) bool {
	_, ok := bean.(web.Controller)
	return ok
}

func (a *ApiGenerator) Do(bean bean.Bean) error {
	a.root = a.lock.RequiredRoot()
	defer a.lock.Release()
	err := os.Chdir(a.root)
	if err != nil {
		return err
	}
	ctr := bean.(web.Controller)
	apiFileName := filepath.Join(a.root, "api", util.GetBeanFileName(bean))
	countFunc, err := a.GenerateApiHandler(ctr, apiFileName)
	if err != nil {
		return err
	}
	err = a.GenerateServiceFactory(ctr, apiFileName)
	if err != nil {
		return err
	}
	if countFunc != 0 {
		fmt.Printf("Gen api: %s with %d function\n", filepath.Base(apiFileName), countFunc)
	}
	a.cmd.Fmt(apiFileName)
	return nil
}

func (a *ApiGenerator) GenerateApiHandler(ctr web.Controller, apiFileName string) (count int, err error) {
	typ := reflect.TypeOf(ctr)
	importSets := list.NewSets()
	err = reflectx.ForEachField(ctr, false, func(field reflect.StructField, value reflect.Value) error {
		if isRestAPI(field) {
			info := controller.NewApiInfo(ctr.Group(), field, nil)
			if _, ok := typ.MethodByName(info.GetMethodName()); !ok {
				beanName := util.GetBeanStructName(ctr)
				// build param
				paramStr, imports, err := a.buildParam(beanName, info)
				if err != nil {
					return err
				}
				importSets.PutAll(imports...)
				// build resp
				response, respImport, err := a.buildResponse(beanName, info)
				if err != nil {
					return err
				}
				importSets.PutAll(respImport)
				//write function
				err = util.WriteFunction(apiFileName, "h", true, beanName, info.GetMethodName(), paramStr, fmt.Sprintf("(%s,error)", response), fmt.Sprintf("associate with %s.%s", beanName, field.Name))
				if err != nil {
					return err
				}
				count++
			}
		}
		return nil
	})
	if err != nil {
		return
	}
	err = util.WriteImports(apiFileName, importSets.ToArray())
	return
}

func (a *ApiGenerator) GenerateServiceFactory(ctr web.Controller, apiFileName string) error {
	singleton := bean_repository.GetSingleton("service_factory")
	if singleton == nil {
		return nil
	}
	factoryTag := fmt.Sprintf(`%s:"service_factory"`, bean_factory.InjectFieldTag)
	contains, err := util.IsFileContains(factoryTag, func(mode int) (*os.File, error) {
		return util.OpenFile(apiFileName, mode)
	})
	if err != nil {
		return err
	}
	if contains {
		return nil
	}
	fmt.Println("add factory:", apiFileName)
	s := fmt.Sprintf("service.Factory `%s`", factoryTag)
	groupTag := fmt.Sprintf("`group:\"%s\"`", ctr.Group())
	err = util.ReplaceFileContents(groupTag, fmt.Sprintf("%s\n%s", groupTag, s), 1, func(mode int) (*os.File, error) {
		return util.OpenFile(apiFileName, mode)
	})
	if err != nil {
		return err
	}
	imports := util.BuildImport("service")
	err = util.WriteImports(apiFileName, []string{imports})
	if err != nil {
		return err
	}
	return nil
}

func (a *ApiGenerator) buildParam(beanName string, info web.ApiInfo) (paramStr string, imports []string, err error) {
	paramBuilder := NewParamBuilder()
	for _, param := range info.GetParams() {
		var pkg string
		var paramType = param.GetSpecifyType()
		if paramType == "" {
			paramType, pkg, err = a.createUnSpecifyParam(param.GetSource(), beanName, info)
		} else {
			paramType, pkg, err = a.createSpecifyParam(paramType, beanName, info)
		}
		if err != nil {
			return
		}
		if pkg != "" {
			imports = append(imports, pkg)
		}
		paramBuilder.AddParam(param.GetName(), paramType)
	}
	paramStr = paramBuilder.Build()
	return
}

func (a *ApiGenerator) createUnSpecifyParam(source, beanName string, info web.ApiInfo) (paramType string, imports string, err error) {
	switch source {
	case web.Body:
		dtoName := beanName + info.GetMethodName() + "Req"
		paramType = "*dto." + dtoName
		imports = util.BuildImport("dto")
		err = a.createDTO(beanName, dtoName, info.GetDescription())
		if err != nil {
			return
		}
		return
	case web.Context:
		paramType = "context.Context"
		imports = "context"
		return
	case web.Request:
		paramType = "*http.Request"
		imports = "net/http"
		return
	case web.Response:
		paramType = "http.ResponseWriter"
		imports = "net/http"
		return
	case web.Stream:
		paramType = "[]byte"
		return
	default:
		paramType = "string"
		return
	}
}

func (a *ApiGenerator) createSpecifyParam(specifyType, beanName string, info web.ApiInfo) (paramType string, imports string, err error) {
	return a.autoGenerateDTO(specifyType, beanName, "Req", info)
}

func (a *ApiGenerator) buildResponse(beanName string, info web.ApiInfo) (paramType string, imports string, err error) {
	resp := info.GetMeta("resp")
	if resp == "" {
		paramType = "string"
		return
	}
	paramType, imports, err = a.autoGenerateDTO(resp, beanName, "Res", info)
	return
}

func (a *ApiGenerator) autoGenerateDTO(specifyType, beanName, suffix string, info web.ApiInfo) (paramType string, imports string, err error) {
	specify := lang.NewString(specifyType)
	if specify.StartWith("dto") || specify.StartWith("[]dto") {
		arr := specify.Split(".")
		switch len(arr) {
		case 1:
			// dto, []dto
			dtoName := beanName + info.GetMethodName() + suffix
			err = a.createDTO(beanName, dtoName, info.GetDescription())
			if err != nil {
				return
			}
			paramType = arr[0].Replace("dto", "*dto", 1).Concat(".", dtoName).String()
		case 2:
			// dto.XXX, []dto.XXX
			err = a.createDTO(beanName, arr[1].String(), info.GetDescription())
			if err != nil {
				return
			}
			paramType = specify.Replace("dto", "*dto", 1).String()
		default:
			err = errors.New(fmt.Sprintf("response define: %s not support", specifyType))
		}
		imports = util.BuildImport("dto")
	} else {
		paramType = specifyType
	}
	return
}

func (a *ApiGenerator) createDTO(beanName string, dtoName string, desc string) error {
	dtoDir := a.root + "dto" + string(os.PathSeparator)
	var dtoFileName = dtoDir + lang.NewString(beanName).Concat("DTO.go").SnakeCase().String()
	defer a.cmd.Fmt(dtoFileName)
	err := util.MkDir(dtoDir)
	if err != nil {
		return err
	}
	// open or create a dto go file
	file := openOrCreateDtoFile(dtoFileName)
	defer file.Close()
	// if exist dto struct
	contains, err := util.IsFileContains(dtoName, func(mode int) (*os.File, error) {
		return util.OpenFile(dtoFileName, mode)
	})
	if err != nil {
		return err
	}
	if contains {
		return nil
	}
	// write dto struct template
	_, err = file.WriteString(fmt.Sprintf(dtoStructTpl, dtoName, desc, dtoName))
	if err != nil {
		return err
	}
	return nil
}

const (
	dtoStructTpl   = "\n// %s %s\ntype %s struct{\n}\n"
	dtoTpl         = "package dto\n"
	paramSeparator = ", "
)

func isRestAPI(field reflect.StructField) bool {
	return field.Type.Implements(reflect.TypeOf(new(web.RestMethod)).Elem())
}

func openOrCreateDtoFile(dtoFileName string) *os.File {
	file, exists, err := util.OpenOrCreateFile(dtoFileName, os.O_APPEND|os.O_RDWR)
	if err != nil {
		log.Fatal(err)
	}
	if !exists {
		_, err = file.WriteString(dtoTpl)
		if err != nil {
			log.Fatal(err)
		}
	}
	return file
}

type ParamBuilder struct {
	paramTypeMapping map[string]string
}

func NewParamBuilder() *ParamBuilder {
	return &ParamBuilder{
		paramTypeMapping: make(map[string]string),
	}
}

func (p *ParamBuilder) AddParam(name, typ string) {
	p.paramTypeMapping[lang.NewString(name).CamelCase().FirstLetterLower().String()] = typ
}

func (p *ParamBuilder) Build() string {
	var params []string
	for name, typ := range p.paramTypeMapping {
		params = append(params, fmt.Sprintf("%s %s", name, typ))
	}
	return strings.Join(params, ", ")
}
